﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
using System.Globalization;
using ICSharpCode.SharpZipLib.GZip;
using System.IO;
using System.Runtime.Serialization;

namespace Memcached.ClientLibrary
{
	internal partial class MemcachedClient
	{
		public bool Set<T>(string key, T value)
			where T : class
		{
			return CatalystSet<T>("set", key, value, DateTime.MaxValue, null, _primitiveAsString);
		}

		public bool Set<T>(string key, T value, int hashCode) 
			where T : class
		{
			return CatalystSet<T>("set", key, value, DateTime.MaxValue, hashCode, _primitiveAsString);
		}

		public bool Set<T>(string key, T value, DateTime expiry) 
			where T : class
		{
			return CatalystSet<T>("set", key, value, expiry, null, _primitiveAsString);
		}

		public bool Set<T>(string key, T value, DateTime expiry, int hashCode)
			where T : class
		{
			return CatalystSet<T>("set", key, value, expiry, hashCode, _primitiveAsString);
		}

		public bool Add<T>(string key, T value)
			where T : class
		{
			return CatalystSet<T>("add", key, value, DateTime.MaxValue, null, _primitiveAsString);
		}

		public bool Add<T>(string key, T value, int hashCode)
			where T : class 
		{
			return CatalystSet<T>("add", key, value, DateTime.MaxValue, hashCode, _primitiveAsString);
		}

		public bool Add<T>(string key, T value, DateTime expiry) 
			where T : class
		{
			return CatalystSet<T>("add", key, value, expiry, null, _primitiveAsString);
		}

		public bool Add<T>(string key, T value, DateTime expiry, int hashCode)
			where T : class
		{
			return CatalystSet<T>("add", key, value, expiry, hashCode, _primitiveAsString);
		}

		public bool Replace<T>(string key, T value)
			where T : class
		{
			return CatalystSet<T>("replace", key, value, DateTime.MaxValue, null, _primitiveAsString);
		}

		public bool Replace<T>(string key, T value, int hashCode)
			where T : class
		{
			return CatalystSet<T>("replace", key, value, DateTime.MaxValue, hashCode, _primitiveAsString);
		}

		public bool Replace<T>(string key, T value, DateTime expiry)
			where T : class
		{
			return CatalystSet<T>("replace", key, value, expiry, null, _primitiveAsString);
		}

		public bool Replace<T>(string key, T value, DateTime expiry, int hashCode)
			where T : class
		{
			return CatalystSet<T>("replace", key, value, expiry, hashCode, _primitiveAsString);
		}

		private bool CatalystSet<T>(string cmdname, string key, T value, DateTime expiry, object hashCode, bool asString)
			where T : class
		{
			if(expiry < DateTime.Now)
				return true;

			if(cmdname == null || cmdname.Trim().Length == 0 || key == null || key.Length == 0) 
			{
				if(log.IsErrorEnabled)
				{
					log.Error(GetLocalizedString("set key null"));
				}
				return false;
			}

			// get SockIO obj
			SockIO sock = SockIOPool.GetInstance(_poolName).GetSock(key, hashCode);
		
			if(sock == null)
				return false;
		
			if(expiry == DateTime.MaxValue)
				expiry = new DateTime(0);

			// store flags
			int flags = 0;
		
			// byte array to hold data
			byte[] val;
			int length = 0;

			// useful for sharing data between .NET and non-.NET
            // and also for storing ints for the increment method
			if(NativeHandler.IsHandled(value)) 
			{
				if(asString) 
				{
                    if(value != null)
                    {
						if(log.IsInfoEnabled)
						{
							log.Info(GetLocalizedString("set store data as string").Replace("$$Key$$", key).Replace("$$Class$$", value.GetType().Name));
						}
                        try
                        {
                            val = UTF8Encoding.UTF8.GetBytes(value.ToString());
							length = val.Length;
                        }
                        catch(ArgumentException ex)
                        {
							if(log.IsErrorEnabled)
							{
								log.Error(GetLocalizedString("set invalid encoding").Replace("$$Encoding$$", _defaultEncoding), ex);
							}
                            sock.Close();
                            sock = null;
                            return false;
                        }
                    }
                    else
                    {
                        val = new byte[0];
						length = 0;
                    }
				}
				else 
				{
					if(log.IsInfoEnabled)
					{
						log.Info(GetLocalizedString("set store with native handler"));
					}

					try 
					{
						val = NativeHandler.Encode(value);
						length = val.Length;
					}
					catch(ArgumentException e) 
					{
						if(log.IsErrorEnabled)
						{
							log.Error(GetLocalizedString("set failed to native handle object"), e);
						}

						sock.Close();
						sock = null;
						return false;
					}
				}
			}
			else 
			{
                if(value != null)
                {
					if(log.IsInfoEnabled)
					{
						log.Info(GetLocalizedString("set serializing".Replace("$$Key$$", key).Replace("$$Class$$", value.GetType().Name)));
					}

                    // always serialize for non-primitive types
					try
                    {
						MemoryStream memStream = Catalyst.Serialization.Serializer.SerializeContract(value, value.GetType());
                        
                        val = memStream.GetBuffer();
						length = (int) memStream.Length;
                        flags |= F_SERIALIZED;
                    }
                    catch(IOException e)
                    {
                        // if we fail to serialize, then
                        // we bail
						if(log.IsErrorEnabled)
						{
							log.Error(GetLocalizedString("set failed to serialize").Replace("$$Object$$", value.ToString()), e);
						}

                        // return socket to pool and bail
                        sock.Close();
                        sock = null;
                        return false;
                    }
                }
                else
                {
                    val = new byte[0];
					length = 0;
                }
			}
		
			// now try to compress if we want to
			// and if the length is over the threshold 
			if(_compressEnable && length > _compressThreshold) 
			{
				if(log.IsInfoEnabled)
				{
					log.Info(GetLocalizedString("set trying to compress data"));
					log.Info(GetLocalizedString("set size prior").Replace("$$Size$$", length.ToString(new NumberFormatInfo())));
				}

				try 
				{
					MemoryStream memoryStream = new MemoryStream();
					GZipOutputStream gos = new GZipOutputStream(memoryStream);
					gos.Write(val, 0, length);
					gos.Finish();
				
					// store it and set compression flag
					val = memoryStream.GetBuffer();
					length = (int)memoryStream.Length;
					flags |= F_COMPRESSED;

					if(log.IsInfoEnabled)
					{
						log.Info(GetLocalizedString("set compression success").Replace("$$Size$$", length.ToString(new NumberFormatInfo())));
					}
				}
				catch(IOException e) 
				{
					if(log.IsErrorEnabled)
					{
						log.Error(GetLocalizedString("set compression failure"), e);
					}
				}
			}

			// now write the data to the cache server
			try 
			{
				string cmd = cmdname + " " + key + " " + flags + " "
					+ GetExpirationTime(expiry) + " " + length + "\r\n";
				sock.Write(UTF8Encoding.UTF8.GetBytes(cmd));
				sock.Write(val,0,length);
				sock.Write(UTF8Encoding.UTF8.GetBytes("\r\n"));
				sock.Flush();

				// get result code
				string line = sock.ReadLine();
				if(log.IsInfoEnabled)
				{
					log.Info(GetLocalizedString("set memcached command result").Replace("$$Cmd$$", cmd).Replace("$$Line$$", line));
				}

				if(STORED == line) 
				{
					if(log.IsInfoEnabled)
					{
						log.Info(GetLocalizedString("set success").Replace("$$Key$$", key));
					}
					sock.Close();
					sock = null;
					return true;
				}
				else if(NOTSTORED == line) 
				{
					if(log.IsInfoEnabled)
					{
						log.Info(GetLocalizedString("set not stored").Replace("$$Key$$", key));
					}
				}
				else 
				{
					if(log.IsErrorEnabled)
					{
						log.Error(GetLocalizedString("set error").Replace("$$Key$$", key).Replace("$$Size$$", length.ToString(new NumberFormatInfo())).Replace("$$Line$$", line));
					}
				}
			}
			catch(IOException e) 
			{
				// exception thrown
				if(log.IsErrorEnabled)
				{
					log.Error(GetLocalizedString("set IOException"), e);
				}

				try 
				{
					sock.TrueClose();
				}
				catch(IOException ioe) 
				{
					if(log.IsErrorEnabled)
					{
						log.Error(GetLocalizedString("failed to close some socket").Replace("$$Socket$$", sock.ToString()), ioe);
					}
				}

				sock = null;
			}

			if(sock != null)
				sock.Close();

			return false;
		}


		public T Get<T>(string key)
			where T : class
		{
			return Get<T>(key, null, false);
		}

		public T Get<T>(string key, int hashCode) 
			where T : class
		{
			return Get<T>(key, hashCode, false);
		}

		public T Get<T>(string key, object hashCode, bool asString) 
			where T : class
		{

			// get SockIO obj using cache key
			SockIO sock = SockIOPool.GetInstance(_poolName).GetSock(key, hashCode);
	    
			if(sock == null)
				return null;

			try 
			{
				string cmd = "get " + key + "\r\n";
				if(log.IsDebugEnabled)
				{
					log.Debug(GetLocalizedString("get memcached command").Replace("$$Cmd$$", cmd));
				}

				sock.Write(UTF8Encoding.UTF8.GetBytes(cmd));
				sock.Flush();

				// build empty map
				// and fill it from server
				Hashtable hm = new Hashtable();
				LoadItems<T>(sock, hm, asString);

				if(log.IsDebugEnabled)
				{
					// debug code
					log.Debug(GetLocalizedString("get memcached result").Replace("$$Results$$", hm.Count.ToString(new NumberFormatInfo())));
				}

				// return the value for this key if we found it
				// else return null 
				sock.Close();
				return (T)hm[key];

			}
			catch(IOException e) 
			{
				// exception thrown
				if(log.IsErrorEnabled)
				{
					log.Error(GetLocalizedString("get IOException").Replace("$$Key$$", key), e);
				}

				try 
				{
					sock.TrueClose();
				}
				catch(IOException ioe) 
				{
					if(log.IsErrorEnabled)
					{
						log.Error(GetLocalizedString("failed to close some socket").Replace("$$Socket$$", sock.ToString()), ioe);
					}
				}
				sock = null;
			}

			if(sock != null)
				sock.Close();

			return null;
		}

        private void LoadItems<T>(SockIO sock, Hashtable hm, bool asString) 
			where T : class
		{
			while(true) 
			{
				string line = sock.ReadLine();
				if(log.IsDebugEnabled)
				{
					log.Debug(GetLocalizedString("loaditems line").Replace("$$Line$$", line));
				}

				if(line.StartsWith(VALUE)) 
				{
					string[] info = line.Split(' ');
					string key    = info[1];
					int flag      = int.Parse(info[2], new NumberFormatInfo());
					int length    = int.Parse(info[3], new NumberFormatInfo());

					if(log.IsDebugEnabled)
					{
						log.Debug(GetLocalizedString("loaditems header").Replace("$$Key$$", key).Replace("$$Flags$$", flag.ToString(new NumberFormatInfo())).Replace("$$Length$$", length.ToString(new NumberFormatInfo())));
					}
				
					// read obj into buffer
					byte[] buf = new byte[length];
					sock.Read(buf);
					sock.ClearEndOfLine();

					// ready object
					object o;
				
					// check for compression
					if((flag & F_COMPRESSED) != 0) 
					{
						try 
						{
							// read the input stream, and write to a byte array output stream since
							// we have to read into a byte array, but we don't know how large it
							// will need to be, and we don't want to resize it a bunch
							GZipInputStream gzi = new GZipInputStream(new MemoryStream(buf));
							MemoryStream bos = new MemoryStream(buf.Length);
							
							int count;
							byte[] tmp = new byte[2048];
							while((count = gzi.Read(tmp, 0, tmp.Length)) > 0)
							{
								bos.Write(tmp, 0, count);
							}
							
							// store uncompressed back to buffer
							buf = bos.ToArray();
							gzi.Close();
						}
						catch(IOException e) 
						{
							if(log.IsErrorEnabled)
							{
								log.Error(GetLocalizedString("loaditems uncompression IOException").Replace("$$Key$$", key), e);
							}
							throw new IOException(GetLocalizedString("loaditems uncompression IOException").Replace("$$Key$$", key), e);
						}
					}

					// we can only take out serialized objects
					if((flag & F_SERIALIZED) == 0) 
					{
						if(_primitiveAsString || asString) 
						{
							// pulling out string value
							if(log.IsInfoEnabled)
							{
								log.Info(GetLocalizedString("loaditems retrieve as string"));
							}
							o = Encoding.GetEncoding(_defaultEncoding).GetString(buf);
						}
						else 
						{
							// decoding object
							try 
							{
								o = NativeHandler.Decode(buf);    
							}
							catch(Exception e) 
							{
								if(log.IsErrorEnabled)
								{
									log.Error(GetLocalizedString("loaditems deserialize error").Replace("$$Key$$", key), e);
								}
								throw new IOException(GetLocalizedString("loaditems deserialize error").Replace("$$Key$$", key), e);
							}
						}
					}
					else 
					{
						// deserialize if the data is serialized
						try 
						{
							MemoryStream memStream = new MemoryStream(buf);
							o = Catalyst.Serialization.Serializer.DeserializeContract(memStream, typeof(T));
							if(log.IsInfoEnabled)
							{
								log.Info(GetLocalizedString("loaditems deserializing").Replace("$$Class$$", o.GetType().Name));
							}
						}
						catch(SerializationException e) 
						{
							if(log.IsErrorEnabled)
							{
								log.Error(GetLocalizedString("loaditems SerializationException").Replace("$$Key$$", key), e);
							}
							throw new IOException(GetLocalizedString("loaditems SerializationException").Replace("$$Key$$", key), e);
						}
					}

					// store the object into the cache
					hm[ key ] =  o ;
				}
				else if(END == line) 
				{
					if(log.IsDebugEnabled)
					{
						log.Debug(GetLocalizedString("loaditems finished"));
					}
					break;
				}
			}
		}
	}
}
